package biz.datalk.industrialland.common.advice;

import biz.datalk.commons.utils.BasicParserUtils;
import biz.datalk.commons.utils.json.JsonUtil;
import biz.datalk.industrialland.common.advice.cfg.DatalkCryptoCfgs;
import biz.datalk.industrialland.common.component.AppEncryptSecurityKeyComponent;
import biz.datalk.industrialland.common.def.CommonConstant;
import biz.datalk.industrialland.common.encrypt.AESTool;
import biz.datalk.industrialland.common.encrypt.RSATool;
import biz.datalk.industrialland.common.encrypt.annotation.RSAES;
import biz.datalk.industrialland.common.encrypt.sm.GMUtils;
import biz.datalk.industrialland.common.exception.ValidateException;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * @author tarofang@163.com
 * @date 2019-08-25
 */
@SuppressWarnings({"NullableProblems", "RedundantThrows"})
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@RestControllerAdvice
@ConditionalOnProperty(name = {"datalk.crypto.enable", "datalk.crypto.request-decrypt"}, havingValue = "true")
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {

    private static final Logger logger = LoggerFactory.getLogger(DecryptRequestBodyAdvice.class);

    private final DatalkCryptoCfgs datalkCryptoCfgs;

    @Autowired
    public DecryptRequestBodyAdvice(DatalkCryptoCfgs datalkCryptoCfgs) {
        this.datalkCryptoCfgs = datalkCryptoCfgs;
    }

    @Autowired
    private AppEncryptSecurityKeyComponent appEncryptSecurityKeyComponent;

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        logger.debug("DecryptRequestBodyAdvice.supports into");
        try {
            if (!datalkCryptoCfgs.isEnable()) {
                return false;
            }
            if (!datalkCryptoCfgs.isRequestDecrypt()) {
                return false;
            }

            Method method = methodParameter.getMethod();
            if (method == null) {
                return false;
            }
            boolean methodSupports = method.isAnnotationPresent(RSAES.class);
            logger.debug("DecryptRequestBodyAdvice.supports methodSupports: {}", methodSupports);

            return methodSupports;
        } finally {
            logger.debug("DecryptRequestBodyAdvice.supports out");
        }
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        String methodOrClazz = "";
        try {
            // 注解在方法上
            Method method = methodParameter.getMethod();
            if (method == null) {
                logger.debug("Invalid. Method must not be null.");
                return inputMessage;
            }
            methodOrClazz = method.getName();
            if (method.isAnnotationPresent(RSAES.class)) {
                RSAES rsaes = methodParameter.getMethodAnnotation(RSAES.class);
                if (rsaes != null && rsaes.inDecrypt()) {
                    logger.info("@RSAES ----- method:[{}] ------ decrypt", method.getName());
                    return new RsaesHttpInputMessage(inputMessage);
                }
            }

            // 注解在类上
            Class<?> clazz = methodParameter.getContainingClass();
            methodOrClazz = clazz.getName();
            if (clazz.isAnnotationPresent(RSAES.class)) {
                RSAES rsaes = clazz.getAnnotation(RSAES.class);
                if (rsaes != null && rsaes.inDecrypt()) {
                    logger.info("@RSAES ----- method:[{}] ------ decrypt", method.getName());
                    return new RsaesHttpInputMessage(inputMessage);
                }
            }

            return inputMessage;
        } catch (Exception ex) {
            logger.warn("no match @AES|@RSA|@RSAES ----- method:[{}] ------ occur exception: {}", methodOrClazz, ex.getMessage(), ex);
            return inputMessage;
        }
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    class RsaesHttpInputMessage implements HttpInputMessage {
        private HttpHeaders headers;
        private InputStream body;

        public RsaesHttpInputMessage(HttpInputMessage inputMessage) throws Exception {
            this.headers = inputMessage.getHeaders();
            //InputStream is = inputMessage.getBody();
            //if (is.markSupported()) {
            //    is.mark(1);
            //    String reqBodyData = IOUtils.toString(is, StandardCharsets.UTF_8);
            //    is.reset();
            //    this.body = (is.read() != -1 ? IOUtils.toInputStream(easpString(reqBodyData), StandardCharsets.UTF_8) : null);
            //} else {
            //    PushbackInputStream pushbackInputStream = new PushbackInputStream(is);
            //    int b = pushbackInputStream.read();
            //    if (b == -1) {
            //        this.body = IOUtils.toInputStream("{}", StandardCharsets.UTF_8);
            //    }
            //    else {
            //        // 退回读取操作
            //        pushbackInputStream.unread(b);
            //        String reqBodyData = IOUtils.toString(pushbackInputStream, StandardCharsets.UTF_8);
            //        pushbackInputStream.unread(b);
            //        this.body = IOUtils.toInputStream(easpString(reqBodyData), StandardCharsets.UTF_8);
            //    }
            //}
            this.body = IOUtils.toInputStream(easpString(IOUtils.toString(inputMessage.getBody(), StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
        }

        @Override
        public InputStream getBody() throws IOException {
            return body;
        }

        @Override
        public HttpHeaders getHeaders() {
            return headers;
        }

        private String easpString(String reqBodyData) {
            logger.debug("request body Data: [{}]", reqBodyData);
            if (StringUtils.isBlank(reqBodyData)) {
                logger.warn("Request Body Data is null or empty.");
                throw new ValidateException("未知请求， 您的访问已被拒绝");
            }

            CryptoDTO convert = convert(reqBodyData);
            if (convert == null || StringUtils.isBlank(convert.getEnkey()) || StringUtils.isBlank(convert.getEndata())) {
                logger.warn("Invalid request Body Data: {}", reqBodyData);
                throw new ValidateException("未知请求， 您的访问已被拒绝");
            }

            List<String> clientTypeList = this.headers.getOrEmpty(CommonConstant.X_CLIENT_TYPE);
            List<String> clientVerList = this.headers.getOrEmpty(CommonConstant.X_CLIENT_VER);
            String clientType = CollectionUtils.isEmpty(clientTypeList) ? StringUtils.EMPTY : clientTypeList.get(0);
            String clientVer = CollectionUtils.isEmpty(clientVerList) ? StringUtils.EMPTY : clientVerList.get(0);
            int clientVerInt = BasicParserUtils.parseInt(clientVer, 0);


            // 获取解密私钥
            boolean useGm = datalkCryptoCfgs.useGm(clientType, clientVerInt);
            AppSecurityInfo appSecurityInfo = appEncryptSecurityKeyComponent.getByAppKey(convert.getAppkey());
            String serverPrivateKey;
            if (appSecurityInfo != null) {
                serverPrivateKey = appSecurityInfo.getServerPrivateKey();
            } else {
                serverPrivateKey = useGm ? datalkCryptoCfgs.getGmServer().getPrivateKey() : datalkCryptoCfgs.getServer().getPrivateKey();
            }
            if (StringUtils.isBlank(serverPrivateKey)) {
                logger.warn("serverPrivateKey is blank. [appkey={}, useGm={}]", convert.getAppkey(), useGm);
            }

            // 解密
            String decrypt;
            if (useGm) {
                String sm4Key = GMUtils.sm2Decrypt(serverPrivateKey, convert.getEnkey());
                decrypt = GMUtils.sm4Decrypt(convert.getEndata(), sm4Key);
            } else {
                String aseKey = RSATool.decrypt(convert.getEnkey(), serverPrivateKey);
                decrypt = AESTool.decrypt(convert.getEndata(), aseKey);
            }

            // String aseKey = RSATool.decrypt(convert.getEnkey(), datalkCryptoCfgs.getServer().getPrivateKey().replaceAll("\\s", ""));
            // String decrypt = AESTool.decrypt(convert.getEndata(), aseKey);
            logger.debug("body params decrypt: [{}]", decrypt);
            return decrypt;
        }
    }

    private CryptoDTO convert(String bodyData) {
        return JsonUtil.json2JavaBean(bodyData, CryptoDTO.class);
    }
}
